/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package Utility;

import java.io.IOException;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Scanner;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * This utility provides server-side functionality for the JCU Message System.
 * 
 * @author Leenix
 */
public class ChatServer {

	private ServerSocket serverSocket;
	private ListenerThread listenerThread;
	private ArrayList<ConnectionThread> connectionThreads;
	private ArrayList<User> userList;
	private ArrayList<String> chatroomList;

	private ArrayList<ForumMessage> forumPostArray;
	private ForumMessage post;

	private boolean running = false;
	private int portNumber;
	private static final Logger logger = Logger.getLogger("ChatServer");

	// The split character is the EOTB character (End of Transmission Block)
	private static final String SPLIT_CHAR = "" + ((char) 23);

	/**
	 * Create and initialise a new ChatServer.
	 */
	public ChatServer() {
		connectionThreads = new ArrayList<ConnectionThread>();
		userList = new ArrayList<User>();

		forumPostArray = new ArrayList<ForumMessage>();

		chatroomList = new ArrayList<String>();
		chatroomList.add("default");
		chatroomList.add("VIP");

		logger.setLevel(Level.INFO);
		new SimpleDateFormat("hh:mm aa");
	}

	/**
	 * Check the current configuration and start listening on the specified port
	 * if it is in range.
	 */
	public void start() {
		if (running) {
			logger.info("Server already running.");
			return;
		}
		if (!isValidPortNumber(portNumber)) {
			logger.info("Invalid port number.");
			return;
		}

		// Start a new listener Thread
		listenerThread = new ListenerThread();
		listenerThread.start();
	}

	/**
	 * Shut down all active connections and listener threads.
	 */
	public void stop() {
		// Guard - Exit early if the server isn't running.
		if (!running) {
			return;
		}

		// Soft shut down connection threads (no need for interrupts).
		running = false;

		listenerThread.interrupt();
		try {
			serverSocket.close();
		} catch (Exception e) {
			logger.severe("Error closing socket.");
		}

		for (User user : userList) {
			user.logout();
		}

		connectionThreads.clear();
	}

	/**
	 * Set the port number for the Server's listening socket.
	 * 
	 * @param portNum
	 *            Listening port number of the server.
	 */
	public void setPortNumber(int portNum) {
		portNumber = portNum;
	}

	/**
	 * Get an arrayList containing the server's connection threads.
	 * 
	 * @return An arrayList containing the active connection threads of the
	 *         server to its clients.
	 */
	public ArrayList<ConnectionThread> getConnectionThreads() {
		return connectionThreads;
	}

	/**
	 * Get the server's splitter character.
	 * 
	 * @return The splitter character used to format the data protocol for
	 *         messages.
	 */
	public String getSplitChar() {
		return SPLIT_CHAR;
	}

	/**
	 * Get the server's list of registered users.
	 * 
	 * @return ArrayList containing registered user objects.
	 */
	public ArrayList<User> getUserList() {
		return userList;
	}

	public int findConnectedUser(String username) {
		for (int i = 0; i < connectionThreads.size(); i++) {
			if (connectionThreads.get(i).getName().equals(username)) {
				return i;
			}
		}

		return -1;
	}

	/**
	 * Check if the current set port number is valid.
	 * 
	 * @param portNum
	 *            The listening port number of the server.
	 * @return Returns true if the set port number is within the valid range.
	 */
	public boolean isValidPortNumber(int portNum) {
		return (portNum >= 49152 && portNum <= 65535);
	}

	/**
	 * Return the amount of current client connections.
	 * 
	 * @return The number of users actively connected to the server.
	 */
	public int getNumConnections() {
		return connectionThreads.size();
	}

	/**
	 * Check if the server is running.
	 * 
	 * @return Returns true if the server is running.
	 */
	public boolean isRunning() {
		return running;
	}

	/**
	 * Create a new chatroom channel for the drop-in group chat.
	 * 
	 * @param chatroomName
	 *            The name of the new chatroom.
	 */
	public void createChatroom(String chatroomName) {
		// Do not allow blank chatroom names
		if (chatroomName.isEmpty()) {
			return;
		}

		// Do not allow duplicate channels
		for (String name : chatroomList) {
			if (name.equals(chatroomName)) {
				return;
			}
		}

		chatroomList.add(chatroomName);
		broadcastChatroomList();
	}

	/**
	 * Broadcasts the list of current chatroom channels.
	 */
	private void broadcastChatroomList() {
		broadcastMessage("CHATROOMS" + SPLIT_CHAR + getChatroomList());
	}

	private void broadcastForumPosts(ForumMessage post) {

		for (ConnectionThread c : connectionThreads) {
			if (post.getAllowedUserList().contains(c.getName())) {
				c.sendMessage("POST" + SPLIT_CHAR + post.getPostTitle() + SPLIT_CHAR + 
						post.getAllowedUserList() + SPLIT_CHAR + post.getsender() + SPLIT_CHAR + post.getReplies());
			}
		}
	}

	/**
	 * Sever the connections with all currently connected clients. (Potential
	 * tongue twister in the making)
	 */
	public void purgeClients() {
		broadcastMessage("STOP" + SPLIT_CHAR + "Server shutting down.");
	}

	/**
	 * Send a message to all connected users.
	 * 
	 * @param message
	 *            Formatted string to be sent to all users.
	 */
	public void broadcastMessage(String message) {
		for (ConnectionThread connection : connectionThreads) {
			connection.sendMessage(message);
		}
	}

	/**
	 * Send a comma-separated-variable list of all past and present users to all
	 * connected users.
	 */
	public void broadcastUserList() {
		String activeUsers = getRegisteredUserList();
		broadcastMessage("USERS" + SPLIT_CHAR + activeUsers);
	}

	/**
	 * Get a comma-separated list of the server's current chatroom channels.
	 * 
	 * @return Csv string of the server's chatrooms.
	 */
	private String getChatroomList() {
		String chatroomString = chatroomList.toString();
		chatroomString = chatroomString.substring(1, chatroomString.length() - 1);
		return chatroomString;
	}

	/**
	 * Produce a comma separated variable string of all the past and present
	 * users registered with the server. Note - This list should always return a
	 * non-empty string.
	 * 
	 * @return Csv string of all registered users.
	 */
	public String getRegisteredUserList() {
		String userString = userList.toString();

		userString = userString.substring(1, userString.length() - 1);

		System.out.println("Users: " + userString);
		return userString;
	}

	/**
	 * Kick a connected user from the server while also delivering the reason
	 * for the kick.
	 * 
	 * @param username
	 *            Username of user to be kicked.
	 * @param kickReason
	 *            Kick message the user will receive.
	 */
	public void kickConnectedUser(String username, String kickReason) {
		int i = findConnectedUser(username);
		connectionThreads.get(i).kickUser(kickReason);
	}

	/**
	 * Sets the user's status to offline in the user list.
	 * 
	 * @param name
	 *            The name of the user to be logged off.
	 */
	public void logoffUser(String name) {
		int i = findConnectedUser(name);
		if (i < 0) {
			return;
		} else {
			userList.get(i).logout();
		}
	}

	public void createForumPost(String[] inputs) {
		ForumMessage post = new ForumMessage(inputs[1], inputs[2], inputs[3], inputs[4]);
		System.out.println("the forum post title created is : " + (post.getPostTitle()).toString() + " userlist is : " + (post.getAllowedUserList()).toString() + " message : " + post.getMessage());
		//ArrayList<ForumMessage> forumPostArray = new ArrayList<ForumMessage>();

		forumPostArray.add(post);
		broadcastForumPosts(post);

		return;

	}

	public void createForumReply(String[] inputs) {
		ForumMessage reply = new ForumMessage(inputs[1], inputs[2], inputs[3], inputs[4]);

		// where reply's postID matches the postID of a post, append to that
		// ArrayList
		String target = reply.getPostTitle();

		// using arrayList method
		for (ForumMessage post : forumPostArray) {
			System.out.println(post.toString());
			if (post.getPostTitle().equals(target)) {

				post.addReply(reply.getMessage());
			} else {
				return;
			}
		}
	};

	/**
	 * ListenerThread Listens on the specified port for connections to the
	 * server. Once a connection is established, a separate connection thread is
	 * created to manage the connection and the listening thread waits for
	 * another connection.
	 */
	class ListenerThread extends Thread implements Runnable {

		@Override
		public void run() {

			logger.info("Listener started...");

			try {
				// Begin listening on server socket.
				serverSocket = new ServerSocket(portNumber);

				running = true;
				while (running) {
					// Accept new connections and assign them to new connection
					// threads to be managed
					final Socket connectionSocket = serverSocket.accept();

					ConnectionThread connectionThread = new ConnectionThread(connectionSocket);
					connectionThreads.add(connectionThread);
					connectionThread.start();
					logger.fine("Connection established.");
				}

			} catch (Exception e) {
				logger.info("Listener thread has stopped.");
			}
		}
	}

	/**
	 * Runnable thread that manages client connections to the server.
	 * Multi-threading used in order to send and receive message simultaneously.
	 */
	class ConnectionThread extends Thread implements Runnable {

		private Socket connectionSocket;
		boolean connected = false;
		private Scanner reader;
		private PrintWriter writer;
		private Thread incomingMessageListener;
		private ArrayList<String> outQueue;

		private long timeOfLastMessage;

		/**
		 * Start a new connection thread to manage a client connection. Stops
		 * running when 'connected' variable is set to false.
		 * 
		 * @param conn
		 *            Active socket connection to client.
		 */
		public ConnectionThread(Socket conn) {
			this.connectionSocket = conn;
			outQueue = new ArrayList<String>();

			timeOfLastMessage = 0;

			connected = true;
		}

		/**
		 * Take an input string from the client and perform an operation based
		 * on the client-server chat data protocol.
		 * 
		 * @param request
		 *            The string command and arguments from the client.
		 * @return True if the operation is successful, false if an error
		 *         occurs.
		 */
		public void handleRequest(String request) throws InterruptedException {
			String[] inputs = request.split(SPLIT_CHAR);
			resetTimeOfLastMessage(timeOfLastMessage, this);

			try {
				// LOGOFF - Close the connection with the client and update the user's status to offline in the database.
				if (inputs[0].startsWith("LOGOUT")) {
					processLogoutRequest();

					// GET_USERS - Send a csv list of the active users.
				} else if (inputs[0].startsWith("GET_USERS")) {
					processGetUsersRequest();

					// GET_CHATROOMS - Send a csv list of chatrooms.
				} else if (inputs[0].startsWith("GET_CHATROOMS")) {
					sendChatroomList();

					// GET_FORUMPOSTS - Send a csv list of posts.
				} else if (inputs[0].startsWith("GET_FORUMPOSTS")) {
					sendForumPostTitles();

					// LOGIN,<Name> - Log the user in with the server arrayList and match the connection thread name with the user.
				} else if (inputs[0].startsWith("LOGIN")) {
					String username = inputs[1];
					processLoginRequest(username);

					// GROUP,<chatroom ID>,<message> - Broadcast a chatroom message.
				} else if (inputs[0].startsWith("GROUP")) {
					broadcastGroupMessage(inputs[1], inputs[2]);

					// POST,
				} else if (inputs[0].startsWith("POST")) {
					createForumPost(inputs);
					System.out.println("POST RECEIVED : " + inputs[4].toString());
					

					// REPLY
				} else if (inputs[0].startsWith("REPLY")) {
					createForumReply(inputs);
					System.out.println("REPLY RECEIVED : " + inputs[4]);
					sendForumPosts();
				}
			} catch (IndexOutOfBoundsException ex) {
			}

		}

		public void sendForumPosts() {
			String posts = (getForumPosts());
			sendMessage("POSTS" + SPLIT_CHAR + posts);
			System.out.println("SENT FORUM POSTS : " + posts);

		}
		
		public void sendForumPostTitles(){
			String forumPostTitles = "";
			for (ForumMessage post : forumPostArray){
				if(post.getAllowedUserList().contains(this.getName()))
				{
				forumPostTitles += post.getPostTitle() + ",";
				}
			}
			sendMessage("FORUM" + SPLIT_CHAR + forumPostTitles);
		}

		public String getForumPosts() {
			String intendedUser = this.getName();

			for (ForumMessage post : forumPostArray) {
				if (post.getAllowedUserList().contains(intendedUser)) {
					sendMessage("POSTS" + SPLIT_CHAR + post.toString());
				}
			}
			return null;
		}

		/**
		 * Reset the time of the 'last' message received to the current time.
		 * Admittedly, this method is only used for point-cutting at this point
		 * in time.
		 * 
		 * @param name
		 *            Name of the user that has received the message.
		 * @param lastMessageTime
		 *            The timestamp of the previous message.
		 */
		private void resetTimeOfLastMessage(long lastMessageTime, ConnectionThread conn) {
			this.timeOfLastMessage = System.currentTimeMillis();
		}

		/**
		 * Log in/Register a user to the server and associate them with their
		 * current connection thread.
		 * 
		 * @param username
		 *            Name of the user.
		 * @throws InterruptedException
		 *             Exception that can be encountered if the thread is
		 *             interrupted when it waits for its reply to propagate.
		 */
		private void processLoginRequest(String username) throws InterruptedException {
			int i = findConnectedUser(username);

			if (i > 0) {
				// User already logged in. Reject!
				if (userList.get(i).loggedIn()) {
					rejectLogin();

				} else {
					userList.get(i).login();
					acceptLogin();
				}
			} else {
				registerNewUser(username);
				acceptLogin();
			}
		}

		/**
		 * Log the user in and send a positive reply to their login request.
		 */
		private void acceptLogin() {
			sendMessage("LOGIN" + SPLIT_CHAR + "OK");
			broadcastUserList();
		}

		/**
		 * Send a rejection message to the client's login request and close the
		 * connection
		 */
		private void rejectLogin() {
			try {
				sendMessage("LOGIN" + SPLIT_CHAR + "ERROR");
				Thread.sleep(50);
				closeConnection();
			} catch (InterruptedException e) {
			}
		}

		/**
		 * Register a new user to the server's records.
		 * 
		 * @param username
		 *            Name of the new user.
		 */
		private void registerNewUser(String username) {
			User newUser = new User(username);
			userList.add(newUser);
			newUser.login();
			this.setName(username);
		}

		/**
		 * Send the server's chatroom list to the current client.
		 */
		private void sendChatroomList() {
			String chatrooms = getChatroomList();
			sendMessage("CHATROOMS" + SPLIT_CHAR + chatrooms);
		}

		/**
		 * Send the server's list of registered username to the current client.
		 */
		private void processGetUsersRequest() {
			String userString = getRegisteredUserList();
			sendMessage("USERS" + SPLIT_CHAR + userString);
		}

		/**
		 * Log the user off and close their connection to the server.
		 */
		private void processLogoutRequest() {
			logger.log(Level.FINE, "Connection closed: [{0}]", this.getName());
			closeConnection();
		}

		/**
		 * Broadcast a chatroom message to all connected clients.
		 * 
		 * @param chatroomID
		 *            The name of the chatroom.
		 * @param message
		 *            Message to be written to the chatroom.
		 */
		private void broadcastGroupMessage(String chatroomID, String message) {
			message = this.getName() + ": " + message;
			broadcastMessage("GROUP" + SPLIT_CHAR + chatroomID + SPLIT_CHAR + message);
		}

		/**
		 * Remotely shut down a user's client, and sever the connection.
		 * 
		 * @param kickReason
		 *            The message the user will receive before remote shutdown.
		 */
		public void kickUser(String kickReason) {
			String kickMessage = "Kicking user [" + this.getName() + "]: " + kickReason;
			sendMessage("STOP" + SPLIT_CHAR + kickMessage);

			// Allow shutdown command to propagate.
			try {
				Thread.sleep(200);
			} catch (InterruptedException e) {
			} finally {
				closeConnection();
			}
		}

		/**
		 * Log off the client and disconnect their connection to the server.
		 */
		private void closeConnection() {
			logoffUser(this.getName());
			connected = false;
			connectionThreads.remove(this);
		}

		/**
		 * Send the client a lockout message to stop the user from sending
		 * messages.
		 */
		public void sendLockoutMessage() {
			sendMessage("LOCKOUT");
		}

		/**
		 * Encrypt and add a single line string message to the outgoing queue to
		 * be sent.
		 * 
		 * @param message
		 *            Single-line string to be sent.
		 */
		public void sendMessage(String message) {
			synchronized (outQueue) {
				outQueue.add(message);
			}
		}

		/**
		 * Start the listeners for incoming and outgoing messages. These loops
		 * will continue to run until the client disconnects or the server shuts
		 * down.
		 */
		@Override
		public void run() {
			try {
				initialiseConnectionStreams();
			} catch (IOException ex) {
				logger.log(Level.SEVERE, null, ex);
			}

			// Create listener thread for receiving.
			incomingMessageListener = new MessageListenerThread();
			incomingMessageListener.start();

			startTransmitLoop();
		}

		/**
		 * Start the message transmission loop. Takes any messages waiting in
		 * the queue and sends them to the client.
		 */
		private void startTransmitLoop() {
			while (running && connected) {
				if (outQueue.isEmpty()) {
					try {
						Thread.sleep(100);
					} catch (InterruptedException ex) {
						Logger.getLogger(ChatServer.class.getName()).log(Level.SEVERE, null, ex);
					}
				} else {
					synchronized (outQueue) {
						for (String msg : outQueue) {
							writer.println(msg);
							writer.flush();
						}
						// Messages sent, wipe queue.
						outQueue.clear();
					}
				}
			}
		}

		/**
		 * Establish output and input streams for the connection.
		 * 
		 * @throws IOException
		 *             Exception when streams not found.
		 */
		private void initialiseConnectionStreams() throws IOException {
			reader = new Scanner(this.connectionSocket.getInputStream());
			writer = new PrintWriter(this.connectionSocket.getOutputStream());
		}

		private class MessageListenerThread extends Thread implements Runnable {
			@Override
			public void run() {
				while (running && connected) {
					try {
						// Wait until a request is received from the client.
						if (reader.hasNextLine()) {
							String requestString = reader.nextLine();
							handleRequest(requestString);
							System.out.println("IN: " + requestString);
						} else {
							Thread.sleep(200);
						}
					} catch (InterruptedException ex) {

					}
				}
			}

		}
	}
}
